﻿/*
** Xin YUAN, 2023, BSD (2)
*/

////////////////////////////////////////////////////////////////////////////////
#ifndef __CTL_SCGI_SERVER_PROTOCOL_H__
#define __CTL_SCGI_SERVER_PROTOCOL_H__
////////////////////////////////////////////////////////////////////////////////

#ifndef __CTL_COLLB_H__
	#error scgi-server.h requires collb.h to be included first
#endif

////////////////////////////////////////////////////////////////////////////////
namespace CTL {
////////////////////////////////////////////////////////////////////////////////

/*
Input:
Length-string(header size):
CONTENT_LENGTH<00>Length-string(body size)<00>
SCGI<00>1<00>
REQUEST_METHOD<00>GET, POST<00>
REQUEST_URI<00>/.../...<00>
[Authorization<00>Bearer XXXX<00>]
...<00>...<00>
...<00><00>
,
[<BODY>]

Output:
Status: 200 OK (400 Bad Request, 500 Internal Server Error)<OD 0A>
Content-Type: text/plain or others (text/html; charset=UTF-8)(application/json; charset=UTF-8)<0D0A>
[Content-Encoding: gzip<0D0A>]
Content-Length: Length-string<0D0A>
<0D0A>
[<BODY>]
*/

//ScgiServerProtocol

class ScgiServerProtocol
{
public:
	//states
	enum {
		STATE_STOP = 0,
		STATE_START = 1,
		STATE_IN_HEADERS,
		STATE_IN_BODY
	};

	//header types
	enum {
		HEADER_OK = 0,
		HEADER_REQUEST_ERROR,
		HEADER_RESPONSE_ERROR
	};

	//body types
	enum {
		BODY_PLAIN = 0,
		BODY_HTML,
		BODY_CSS,  // text/css
		BODY_JS,  // application/x-javascript
		BODY_JSON,
		BODY_IMAGE_GIF,  // image/gif
		BODY_IMAGE_JPEG,  // image/jpeg
		BODY_IMAGE_PNG  // image/png
	};

	//buffer
	enum {
		BUFFER_SIZE = 16384
	};

public:
	ScgiServerProtocol() noexcept
	{
		init();
	}
	ScgiServerProtocol(const ScgiServerProtocol&) = delete;
	~ScgiServerProtocol() noexcept
	{
	}

	ScgiServerProtocol& operator=(const ScgiServerProtocol&) = delete;

	int GetState() const noexcept
	{
		return m_iState;
	}

	void Reset() noexcept
	{
		m_buffer.SetSize(0);
		init();
	}

	bool Input(const uint8_t* p, uint32_t size) noexcept
	{
		uintptr_t uOldSize = m_buffer.GetSize();
		//overflow or too large
		if ( uOldSize + size < size || uOldSize + size > BUFFER_SIZE )
			return false;
		//append
		if ( !m_buffer.Append(size) )
			return false;
		MemoryTraits::Copy(p, size, m_buffer.GetAddress() + uOldSize);
		bool b = true;
		//states
		while ( b && m_iState != STATE_STOP && m_last < m_buffer.GetSize() ) {
			switch ( m_iState ) {
			case STATE_START:
				b = process_headers_size();
				break;
			case STATE_IN_HEADERS:
				b = process_header();
				break;
			default:
				b = false;
				break;
			}
		}
		return b;
	}
	bool InputNextBodySize(uint32_t size) noexcept
	{
		assert( m_iState == STATE_IN_BODY );
		if ( m_body_size < size )
			return false;
		m_body_size -= size;
		if ( m_body_size == 0 )
			m_iState = STATE_STOP;
		return true;
	}

	const char* GetUrl() const noexcept
	{
		return m_url_start == 0 ? NULL : (const char*)m_buffer.GetAddress() + m_url_start;
	}
	const char* GetAuth() const noexcept
	{
		return m_auth_start == 0 ? NULL : (const char*)m_buffer.GetAddress() + m_auth_start;
	}
	const char* GetCurrentBody(uint32_t& len) const noexcept
	{
		len = (uint32_t)(m_last - m_start);
		return m_body_start == 0 ? NULL : (const char*)m_buffer.GetAddress() + m_body_start;
	}

	bool OutputHeaders(int iHeaderType, int iBodyType, uint32_t uBodySize,
				uint8_t* szBuffer, uint32_t uBufferSize,
				uint32_t& uActSize) noexcept
	{
		constexpr const char c_plain[] = "text/plain; charset=UTF-8";
		constexpr const uintptr_t c_plain_len = sizeof(c_plain) / sizeof(char) - 1;
		constexpr const char c_html[] = "text/html; charset=UTF-8";
		constexpr const uintptr_t c_html_len = sizeof(c_html) / sizeof(char) - 1;
		constexpr const char c_css[] = "text/css; charset=UTF-8";
		constexpr const uintptr_t c_css_len = sizeof(c_css) / sizeof(char) - 1;
		constexpr const char c_js[] = "application/x-javascript; charset=UTF-8";
		constexpr const uintptr_t c_js_len = sizeof(c_js) / sizeof(char) - 1;
		constexpr const char c_json[] = "application/json; charset=UTF-8";
		constexpr const uintptr_t c_json_len = sizeof(c_json) / sizeof(char) - 1;
		constexpr const char c_gif[] = "image/gif";
		constexpr const uintptr_t c_gif_len = sizeof(c_gif) / sizeof(char) - 1;
		constexpr const char c_jpeg[] = "image/jpeg";
		constexpr const uintptr_t c_jpeg_len = sizeof(c_jpeg) / sizeof(char) - 1;
		constexpr const char c_png[] = "image/png";
		constexpr const uintptr_t c_png_len = sizeof(c_png) / sizeof(char) - 1;

		constexpr const char c_ok[] = "200 OK";
		constexpr const uintptr_t c_ok_len = sizeof(c_ok) / sizeof(char) - 1;
		constexpr const char c_request[] = "400 Bad Request";
		constexpr const uintptr_t c_request_len = sizeof(c_request) / sizeof(char) - 1;
		constexpr const char c_response[] = "500 Internal Server Error";
		constexpr const uintptr_t c_response_len = sizeof(c_response) / sizeof(char) - 1;

		constexpr const char c_status[] = "Status: ";
		constexpr const uintptr_t c_status_len = sizeof(c_status) / sizeof(char) - 1;
		constexpr const char c_type[] = "Content-Type: ";
		constexpr const uintptr_t c_type_len = sizeof(c_type) / sizeof(char) - 1;
		constexpr const char c_length[] = "Content-Length: ";
		constexpr const uintptr_t c_length_len = sizeof(c_length) / sizeof(char) - 1;

		uActSize = uBufferSize;
		const char* sz = NULL;
		uintptr_t len = 0;

		//status
		sz = c_status;
		len = c_status_len;
		if ( (uintptr_t)uActSize < len )
			return false;
		MemoryTraits::Copy(sz, len, szBuffer);
		szBuffer += len;
		uActSize -= (uint32_t)len;
		sz = NULL;
		len = 0;
		switch ( iHeaderType ) {
		case HEADER_OK:
			sz = c_ok;
			len = c_ok_len;
			break;
		case HEADER_REQUEST_ERROR:
			sz = c_request;
			len = c_request_len;
			break;
		case HEADER_RESPONSE_ERROR:
			sz = c_response;
			len = c_response_len;
			break;
		default:
			break;
		}
		if ( sz == NULL )
			return false;
		len += 2;
		if ( (uintptr_t)uActSize < len )
			return false;
		MemoryTraits::Copy(sz, len - 2, szBuffer);
		szBuffer[len - 2] = 0x0D;
		szBuffer[len - 1] = 0x0A;
		szBuffer += len;
		uActSize -= (uint32_t)len;

		//content type
		sz = c_type;
		len = c_type_len;
		if ( (uintptr_t)uActSize < len )
			return false;
		MemoryTraits::Copy(sz, len, szBuffer);
		szBuffer += len;
		uActSize -= (uint32_t)len;
		sz = NULL;
		len = 0;
		switch ( iBodyType ) {
		case BODY_PLAIN:
			sz = c_plain;
			len = c_plain_len;
			break;
		case BODY_HTML:
			sz = c_html;
			len = c_html_len;
			break;
		case BODY_CSS:
			sz = c_css;
			len = c_css_len;
			break;
		case BODY_JS:
			sz = c_js;
			len = c_js_len;
			break;
		case BODY_JSON:
			sz = c_json;
			len = c_json_len;
			break;
		case BODY_IMAGE_GIF:
			sz = c_gif;
			len = c_gif_len;
			break;
		case BODY_IMAGE_JPEG:
			sz = c_jpeg;
			len = c_jpeg_len;
			break;
		case BODY_IMAGE_PNG:
			sz = c_png;
			len = c_png_len;
			break;
		default:
			break;
		}
		if ( sz == NULL )
			return false;
		len += 2;
		if ( (uintptr_t)uActSize < len )
			return false;
		MemoryTraits::Copy(sz, len - 2, szBuffer);
		szBuffer[len - 2] = 0x0D;
		szBuffer[len - 1] = 0x0A;
		szBuffer += len;
		uActSize -= (uint32_t)len;

		//content length
		sz = c_length;
		len = c_length_len;
		if ( (uintptr_t)uActSize < len )
			return false;
		MemoryTraits::Copy(sz, len, szBuffer);
		szBuffer += len;
		uActSize -= (uint32_t)len;
		char buf[64] = { 0 };
		::snprintf(buf, sizeof(buf) / sizeof(char), "%u", uBodySize);
		len = (uint32_t)CharTraits<char>::CalcLength(buf);
		len += 2;
		if ( (uintptr_t)uActSize < len )
			return false;
		MemoryTraits::Copy(buf, len - 2, szBuffer);
		szBuffer[len - 2] = 0x0D;
		szBuffer[len - 1] = 0x0A;
		szBuffer += len;
		uActSize -= (uint32_t)len;

		//end
		len = 2;
		if ( (uintptr_t)uActSize < len )
			return false;
		szBuffer[len - 2] = 0x0D;
		szBuffer[len - 1] = 0x0A;
		szBuffer += len;
		uActSize -= (uint32_t)len;

		uActSize = uBufferSize - uActSize;
		return true;
	}

private:
	//XXX:
	bool process_headers_size() noexcept
	{
		uintptr_t size = m_buffer.GetSize();
		for ( uintptr_t i = m_last; i < size; i ++ ) {
			if ( m_buffer[i] == ':' ) {
				if ( !extract_number(m_start, i, m_uHeaderSize) )
					return false;
				m_buffer.RemoveAt(m_start, i - m_start + 1);
				m_last = m_start;
				m_iState = STATE_IN_HEADERS;
				m_value_start = m_start;
				return true;
			}
		}
		m_last = size;
		return true;
	}
	//KEY<00>[VALUE]<00>
	bool process_header() noexcept
	{
		uintptr_t size = m_buffer.GetSize();
		if ( m_buffer[m_start] == ',' ) {
			if ( m_value_start != m_start ) //not see [VALUE]<00>
				return false;
			if ( m_url_start == 0 || m_iMethod == 0 ) //has not URL or METHOD
				return false;
			if ( m_uHeaderSize != 0 )
				return false;
			//to body
			m_buffer.RemoveAt(m_start, 1);
			m_last = m_buffer.GetSize();
			m_body_start = m_start;
			if ( m_body_size == 0 || m_body_size <= m_last - m_start )
				m_iState = STATE_STOP;
			else
				m_iState = STATE_IN_BODY;
			return true;
		}
		for ( uintptr_t i = m_last; i < size; i ++ ) {
			if ( m_buffer[i] != 0x00 )
				continue;
			//has value
			if ( m_value_start != m_start ) {
				uintptr_t header_size = i - m_start + 1;
				if ( (uintptr_t)m_uHeaderSize < header_size )
					return false;
				m_uHeaderSize -= (uint32_t)header_size;
				//KEY
				const char* szKey = (const char*)m_buffer.GetAddress() + m_start;
				const char* szValue = (const char*)m_buffer.GetAddress() + m_value_start;
				if ( ::strcmp(szKey, "CONTENT_LENGTH") == 0 ) {
					if ( !extract_number(m_value_start, i, m_uBodySize) )
						return false;
					m_buffer.RemoveAt(m_start, header_size);
				}
				else if ( ::strcmp(szKey, "REQUEST_METHOD") == 0 ) {
					if ( ::strcmp(szValue, "POST") == 0 )
						m_iMethod = 2;
					else if ( ::strcmp(szValue, "GET") == 0 )
						m_iMethod = 1;
					else
						return false;
					m_buffer.RemoveAt(m_start, header_size);
				}
				else if ( ::strcmp(szKey, "REQUEST_URI") == 0 ) {
					m_buffer.RemoveAt(m_start, m_value_start - m_start);
					m_url_start = m_start;
					m_start += (i - m_value_start + 1);
				}
				else if ( ::strcmp(szKey, "Authorization") == 0 ) {
					m_buffer.RemoveAt(m_start, m_value_start - m_start);
					m_auth_start = m_start;
					m_start += (i - m_value_start + 1);
				}
				else {
					m_buffer.RemoveAt(m_start, header_size);
				}
				m_last = m_start;
				m_value_start = m_start;
				return true;
			}
			//set start of VALUE
			if ( m_buffer[m_start] == 0x00 )
				return false;
			m_value_start = i + 1;
		}
		m_last = size;
		return true;
	}

	bool extract_number(uintptr_t uStart, uintptr_t uEnd, uint32_t& num) const noexcept
	{
		num = 0;
		uint8_t ch = m_buffer[uStart];
		if ( ch < '1' || ch > '9' )
			return false;
		num = ch - '0';
		for ( uintptr_t i = uStart + 1; i < uEnd; i ++ ) {
			ch = m_buffer[i];
			if ( ch < '0' || ch > '9' )
				return false;
			if ( !ArithmeticTraits<uint32_t>::Multiply(num, 10, num) )
				return false;
			if ( !ArithmeticTraits<uint32_t>::Add(num, ch - '0', num) )
				return false;
		}
		return true;
	}

	void init() noexcept
	{
		m_iState = STATE_START;
		m_start = 0;
		m_last = 0;
		m_uHeaderSize = 0;
		m_uBodySize = 0;
		//data
		m_url_start = 0;
		m_auth_start = 0;
		m_body_start = 0;
		m_body_size = 0;
		m_iMethod = 0;
	}

private:
	int m_iState;

	//buffer
	PlainArray<uint8_t> m_buffer;
	uintptr_t m_start;
	uintptr_t m_last;

	//internal
	uint32_t m_uHeaderSize;
	uint32_t m_uBodySize;

	uintptr_t m_value_start;

	//data
	uintptr_t m_url_start;
	uintptr_t m_auth_start;
	uintptr_t m_body_start;
	uint32_t m_body_size;
	int m_iMethod;  // 0 --- NONE  1 --- GET  2 --- POST
};

////////////////////////////////////////////////////////////////////////////////
}
////////////////////////////////////////////////////////////////////////////////
#endif
////////////////////////////////////////////////////////////////////////////////
